16.6. 结构体

数组和结构体是 C 支持创建数据元素集合的两种方式。数组用于创建相同类型的数据元素的有序集合,而结构体用于创建 不同类型的 数据元素集合。C 程序员可以以多种不同的方式组合数组和结构体构建块,以创建更复杂的数据类型和结构。本节介绍结构体,在第 2 章中,我们更详细地描述结构体展示如何将它们与数组组合

C 不是面向对象语言;因此,它不支持类。但是,它支持定义结构化类型,它们类似于类的公共数据部分。struct 是一种用于表示异构数据集合的类型;它是一种将一组不同类型视为单个、连贯的单元的机制。C 结构在各个数据值之上提供了一个抽象级别,将它们视为单一类型。例如,学生有姓名、年龄、平均绩点 (GPA) 和毕业年份。程序员可以定义一种新的 struct 类型,将这四个数据元素组合成一个 struct student 变量,该变量包含一个姓名值(类型 char [],用于保存字符串)、一个年龄值(类型 int)、一个 GPA 值(类型 float)和一个毕业年份值(类型 int)。这种结构类型的单个变量可以存储特定学生的所有四部分数据;例如(“Freya”,19,3.7,2021)。

在 C 程序中定义和使用struct类型有三个步骤:

  1. 定义一个代表结构的新struct类型。
  2. 声明新的struct类型的变量。
  3. 使用点(.)符号来访问变量的各个字段值。

16.6.1. 定义结构体类型

结构体类型定义应出现在 任何函数的外部 ,通常位于程序的 .c 文件的顶部附近。定义新结构体类型的语法如下(struct 是保留关键字):

struct <struct_name> {
    <field 1 type> <field 1 name>;
    <field 2 type> <field 2 name>;
    <field 3 type> <field 3 name>;
    ...
};

下面是定义一个新的 struct studentT 类型来存储学生数据的示例:

struct studentT {
    char name[64];
    int age;
    float gpa;
    int grad_yr;
};

这个结构体定义在 C 的类型系统中增加了一个新类型,类型名称为 struct studentT。这个结构体定义了四个字段,每个字段定义都包括字段的类型和名称。注意,在这个例子中,name 字段的类型是一个字符数组,用于用作字符串

16.6.2. 声明结构体类型的变量

定义类型后,即可声明新类型struct studentT的变量。请注意,与我们迄今为止遇到的仅由一个单词组成的其他类型(例如intcharfloat)不同,新结构类型的名称由两个单词组成,struct studentT

struct studentT student1, student2; // student1, student2 are struct studentT

16.6.3. 访问字段值

要访问结构变量中的字段值,请使用 点表示法

<variable name>.<field name>

访问结构体及其字段时,请仔细考虑所用变量的类型。C 语言新手程序员经常会因为没有考虑到结构体字段的类型而导致程序中出现错误。表 1 显示了围绕struct studentT类型的几个表达式的类型。

表 1. 与各种 Struct studentT 表达式相关的类型

ExpressionC type
student1struct studentT
student1.ageinteger (int)
student1.namearray of characters (char [])
student1.name[3]character (char), the type stored in each position of the name array

以下是分配struct studentT变量字段的一些示例:

// The 'name' field is an array of characters, so we can use the 'strcpy'
// string library function to fill in the array with a string value.
strcpy(student1.name, "Kwame Salter");

// The 'age' field is an integer.
student1.age = 18 + 2;

// The 'gpa' field is a float.
student1.gpa = 3.5;

// The 'grad_yr' field is an int
student1.grad_yr = 2020;
student2.grad_yr = student1.grad_yr;

图 1 说明了在上例中字段赋值之后,变量 student1 在内存中的布局。只有结构体变量的字段(方框中的区域)存储在内存中。为了清晰起见,图中标记了字段名称,但对于 C 编译器来说,字段只是存储位置或从结构体变量内存起始处的偏移量。例如​​,根据 struct studentT 的定义,编译器知道要访问名为 gpa 的字段,它必须跳过一个包含 64 个字符(name)和一个整数(age)的数组。请注意,在图中,name字段仅描述了 64 个字符数组的前六个字符。

The layout of student1’s memory: the name field is a character array containing 'k' 'w' 'a' 'm' 'e' …​  The age field holds 20, the gpa field stores 3.5, and grad_yr contains 2020.

图 1. 分配每个字段后 student1 变量的内存

C 结构体类型是左值(lvalues),这意味着它们可以出现在赋值语句的左侧。因此,可以使用简单的赋值语句将一个结构体变量赋给另一个结构体变量的值。赋值语句右侧结构体的字段值被复制到赋值语句左侧结构体的字段值。换句话说,一个结构体的内存内容被复制到另一个结构体的内存中。下面是以这种方式分配结构体值的示例:

student2 = student1;  // student2 gets the value of student1
                      // (student1's field values are copied to
                      //  corresponding field values of student2)

strcpy(student2.name, "Frances Allen");  // change one field value

图 2 显示了执行赋值语句和调用 strcpy 后两个学生变量的值。请注意,该图将 name 字段描述为它们包含的字符串值,而不是 64 个字符的完整数组。

Struct Values and Assignment: the field values of the struct on the right hand side are assigned to corresponding field values of the struct on the left hand side of the assignment statement.

图 2. 执行结构赋值和 strcpy 调用后 student1 和 student2 结构的布局

C 提供了一个 sizeof 运算符,它接受一个类型并返回该类型使用的字节数。sizeof 运算符可用于任何 C 类型,包括结构类型,以查看该类型的变量需要多少内存空间。例如,我们可以打印 struct studentT 类型的大小:

// Note: the `%lu` format placeholder specifies an unsigned long value.
printf("number of bytes in student struct: %lu\n", sizeof(struct studentT));

运行时,此行应打印出至少 76 字节的值,因为 name 数组中有 64 个字符(每个 char 1 字节),int age 字段有 4 字节,float gpa 字段有 4 字节,int grad_yr 字段有 4 字节。在某些机器上,确切的字节数可能大于 76。

下面是一个完整示例程序,定义并演示了 struct studentT 类型的用法:

#include <stdio.h>
#include <string.h>

// Define a new type: struct studentT
// Note that struct definitions should be outside function bodies.
struct studentT {
    char name[64];
    int age;
    float gpa;
    int grad_yr;
};

int main(void) {
    struct studentT student1, student2;

    strcpy(student1.name, "Kwame Salter");  // name field is a char array
    student1.age = 18 + 2;                  // age field is an int
    student1.gpa = 3.5;                     // gpa field is a float
    student1.grad_yr = 2020;                // grad_yr field is an int

    /* Note: printf doesn't have a format placeholder for printing a
     * struct studentT (a type we defined).  Instead, we'll need to
     * individually pass each field to printf. */
    printf("name: %s age: %d gpa: %g, year: %d\n",
           student1.name, student1.age, student1.gpa, student1.grad_yr);

    /* Copy all the field values of student1 into student2. */
    student2 = student1;

    /* Make a few changes to the student2 variable. */
    strcpy(student2.name, "Frances Allen");
    student2.grad_yr = student1.grad_yr + 1;

    /* Print the fields of student2. */
    printf("name: %s age: %d gpa: %g, year: %d\n",
           student2.name, student2.age, student2.gpa, student2.grad_yr);

    /* Print the size of the struct studentT type. */
    printf("number of bytes in student struct: %lu\n", sizeof(struct studentT));

    return 0;
}

运行时,该程序输出以下内容:

name: Kwame Salter age: 20 gpa: 3.5, year: 2020
name: Frances Allen age: 20 gpa: 3.5, year: 2021
number of bytes in student struct: 76

结构体是左值(lvalues)

左值 是可以出现在赋值语句左侧的表达式。它是表示内存存储位置的表达式。当我们介绍 C 指针类型以及创建结合 C 数组、结构体和指针的更复杂结构的示例时,重要的是仔细考虑类型并记住哪些 C 表达式是有效的左值(可以在赋值语句的左侧使用)。

从我们目前对 C 的了解来看,基类型的单个变量、数组元素和结构体都是左值。静态声明的数组的名称不是左值(您不能更改内存中静态声明的数组的基地址)。以下示例代码片段根据不同类型的左值状态说明了有效和无效的 C 赋值语句:

struct studentT {
  char name[32];
  int  age;
  float gpa;
  int  grad_yr;
>};

>int main(void) {
   struct studentT  student1, student2;
   int x;
   char arr[10], ch;

   x = 10;                 // Valid C: x is an lvalue
   ch = 'm';               // Valid C: ch is an lvalue
   student1.age = 18;      // Valid C: age field is an lvalue
   student2 = student1;    // Valid C: student2 is an lvalue
   arr[3] = ch;            // Valid C: arr[3] is an lvalue

   x + 1 = 8;       // Invalid C: x+1 is not an lvalue
   arr = "hello";   // Invalid C: arr is not an lvalue
   //  cannot change base addr of statically declared array
   //  (use strcpy to copy the string value "hello" to arr)
   
   student1.name = student2.name;  // Invalid C: name field is not an lvalue
                                // (the base address of a statically
                                //  declared array cannot be changed)	

16.6.4. 将结构体传递给函数

在 C 语言中,所有类型的参数都是通过值传递给函数的。因此,如果一个函数有一个结构体类型参数,那么当使用结构体参数调用时,参数的将传递给其参数,这意味着该参数获得其参数值的副本。结构体变量的值是其内存的内容,这就是为什么我们可以在单个赋值语句中将一个结构的字段赋值给另一个结构体,如下所示:

student2 = student1;

因为结构体变量的值代表其内存的全部内容,所以将结构体作为参数传递给函数会为参数提供该参数结构体所有字段值的副本。如果函数更改了结构体参数的字段值,则对参数字段值的更改不会对参数的相应字段值产生任何影响。也就是说,对参数字段的更改只会修改参数这些字段的内存位置中的值,而不会修改参数这些字段的内存位置中的值。

下面是一个 完整示例程序,其中使用了 checkID 函数,该函数带有一个结构体参数:

#include <stdio.h>
#include <string.h>

/* struct type definition: */
struct studentT {
    char name[64];
    int  age;
    float gpa;
    int  grad_yr;
};

/* function prototype (prototype: a declaration of the
 *    checkID function so that main can call it, its full
 *    definition is listed after main function in the file):
 */
int checkID(struct studentT s1, int min_age);

int main(void) {
    int can_vote;
    struct studentT student;

    strcpy(student.name, "Ruth");
    student.age = 17;
    student.gpa = 3.5;
    student.grad_yr = 2021;

    can_vote = checkID(student, 18);
    if (can_vote) {
        printf("%s is %d years old and can vote.\n",
                student.name, student.age);
    } else {
        printf("%s is only %d years old and cannot vote.\n",
                student.name, student.age);
    }

    return 0;
}

/*  check if a student is at least the min age
 *    s: a student
 *    min_age: a minimum age value to test
 *    returns: 1 if the student is min_age or older, 0 otherwise
 */
int checkID(struct studentT s, int min_age) {
    int ret = 1;  // initialize the return value to 1 (true)

    if (s.age < min_age) {
        ret = 0;  // update the return value to 0 (false)

        // let's try changing the student's age
        s.age = min_age + 1;
    }

    printf("%s is %d years old\n", s.name, s.age);

    return ret;
}

main 调用 checkID 时,student 结构体的值(其所有字段的内存内容的副本)被传递给 s 参数。当函数更改其参数的 age 字段的值时,它不会影响其参数(student)的 age 字段。通过运行程序可以看到此行为,程序输出以下内容:

Ruth is 19 years old
Ruth is only 17 years old and cannot vote.

输出显示,当 checkID 打印 age 字段时,它反映了函数对参数 sage 字段的更改。然而,在函数调用返回后,main 打印的 studentage 字段的值与调用 checkID 之前的值相同。图 3 展示了 checkID 函数返回之前调用堆栈的内容。

As the student struct is passed to checkID, the parameter gets a copy of its contents.  When checkID modifies the age field to 19, the change only applies to its local copy.  The student struct’s age field in main remains at 17.

图 3. checkID 函数返回前的调用堆栈内容

当结构体包含静态声明的数组字段(如 struct studentT 中的 name 字段)时,理解结构体参数的按值传递(pass-by-value)语义尤为重要。当将这样的结构体传递给函数时,结构体参数的整个内存内容(包括数组字段中的每个数组元素)都将复制到其参数中。如果函数更改了参数结构体的数组内容,则这些更改将不会在函数返回后保留。考虑到我们对数组如何传递给函数的了解,这种行为可能看起来很奇怪,但它与前面描述的结构复制行为一致。